diff options
Diffstat (limited to 'src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java')
-rw-r--r-- | src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java new file mode 100644 index 000000000..b2083f858 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java @@ -0,0 +1,380 @@ +package org.yuzu.yuzu_emu.fragments; + +import android.content.Context; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.view.Choreographer; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.yuzu.yuzu_emu.NativeLibrary; +import org.yuzu.yuzu_emu.R; +import org.yuzu.yuzu_emu.activities.EmulationActivity; +import org.yuzu.yuzu_emu.overlay.InputOverlay; +import org.yuzu.yuzu_emu.utils.DirectoryInitialization; +import org.yuzu.yuzu_emu.utils.DirectoryInitialization.DirectoryInitializationState; +import org.yuzu.yuzu_emu.utils.DirectoryStateReceiver; +import org.yuzu.yuzu_emu.utils.EmulationMenuSettings; +import org.yuzu.yuzu_emu.utils.Log; + +public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback, Choreographer.FrameCallback { + private static final String KEY_GAMEPATH = "gamepath"; + + private static final Handler perfStatsUpdateHandler = new Handler(); + + private SharedPreferences mPreferences; + + private InputOverlay mInputOverlay; + + private EmulationState mEmulationState; + + private DirectoryStateReceiver directoryStateReceiver; + + private EmulationActivity activity; + + private TextView mPerfStats; + + private Runnable perfStatsUpdater; + + public static EmulationFragment newInstance(String gamePath) { + Bundle args = new Bundle(); + args.putString(KEY_GAMEPATH, gamePath); + + EmulationFragment fragment = new EmulationFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + if (context instanceof EmulationActivity) { + activity = (EmulationActivity) context; + NativeLibrary.setEmulationActivity((EmulationActivity) context); + } else { + throw new IllegalStateException("EmulationFragment must have EmulationActivity parent"); + } + } + + /** + * Initialize anything that doesn't depend on the layout / views in here. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // So this fragment doesn't restart on configuration changes; i.e. rotation. + setRetainInstance(true); + + mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + + String gamePath = getArguments().getString(KEY_GAMEPATH); + mEmulationState = new EmulationState(gamePath); + } + + /** + * Initialize the UI and start emulation in here. + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View contents = inflater.inflate(R.layout.fragment_emulation, container, false); + + SurfaceView surfaceView = contents.findViewById(R.id.surface_emulation); + surfaceView.getHolder().addCallback(this); + + mInputOverlay = contents.findViewById(R.id.surface_input_overlay); + mPerfStats = contents.findViewById(R.id.show_fps_text); + mPerfStats.setTextColor(Color.YELLOW); + + Button doneButton = contents.findViewById(R.id.done_control_config); + if (doneButton != null) { + doneButton.setOnClickListener(v -> stopConfiguringControls()); + } + + // Show/hide the "Show FPS" overlay + updateShowFpsOverlay(); + + // The new Surface created here will get passed to the native code via onSurfaceChanged. + return contents; + } + + @Override + public void onResume() { + super.onResume(); + Choreographer.getInstance().postFrameCallback(this); + if (DirectoryInitialization.areDirectoriesReady()) { + mEmulationState.run(activity.isActivityRecreated()); + } else { + setupDirectoriesThenStartEmulation(); + } + } + + @Override + public void onPause() { + if (directoryStateReceiver != null) { + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver); + directoryStateReceiver = null; + } + + if (mEmulationState.isRunning()) { + mEmulationState.pause(); + } + + Choreographer.getInstance().removeFrameCallback(this); + super.onPause(); + } + + @Override + public void onDetach() { + NativeLibrary.clearEmulationActivity(); + super.onDetach(); + } + + private void setupDirectoriesThenStartEmulation() { + IntentFilter statusIntentFilter = new IntentFilter( + DirectoryInitialization.BROADCAST_ACTION); + + directoryStateReceiver = + new DirectoryStateReceiver(directoryInitializationState -> + { + if (directoryInitializationState == + DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) { + mEmulationState.run(activity.isActivityRecreated()); + } else if (directoryInitializationState == + DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) { + Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT) + .show(); + } else if (directoryInitializationState == + DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { + Toast.makeText(getContext(), R.string.external_storage_not_mounted, + Toast.LENGTH_SHORT) + .show(); + } + }); + + // Registers the DirectoryStateReceiver and its intent filters + LocalBroadcastManager.getInstance(getActivity()).registerReceiver( + directoryStateReceiver, + statusIntentFilter); + DirectoryInitialization.start(getActivity()); + } + + public void refreshInputOverlay() { + mInputOverlay.refreshControls(); + } + + public void resetInputOverlay() { + // Reset button scale + SharedPreferences.Editor editor = mPreferences.edit(); + editor.putInt("controlScale", 50); + editor.apply(); + + mInputOverlay.resetButtonPlacement(); + } + + public void updateShowFpsOverlay() { + if (true) { + final int SYSTEM_FPS = 0; + final int FPS = 1; + final int FRAMETIME = 2; + final int SPEED = 3; + + perfStatsUpdater = () -> + { + final double[] perfStats = NativeLibrary.GetPerfStats(); + if (perfStats[FPS] > 0) { + mPerfStats.setText(String.format("FPS: %d Speed: %d%%", (int) (perfStats[FPS]), + (int) (perfStats[SPEED] * 100.0))); + } + + perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 3000); + }; + perfStatsUpdateHandler.post(perfStatsUpdater); + + mPerfStats.setVisibility(View.VISIBLE); + } else { + if (perfStatsUpdater != null) { + perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater); + } + + mPerfStats.setVisibility(View.GONE); + } + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + // We purposely don't do anything here. + // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height); + mEmulationState.newSurface(holder.getSurface()); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mEmulationState.clearSurface(); + } + + @Override + public void doFrame(long frameTimeNanos) { + Choreographer.getInstance().postFrameCallback(this); + NativeLibrary.DoFrame(); + } + + public void stopEmulation() { + mEmulationState.stop(); + } + + public void startConfiguringControls() { + getView().findViewById(R.id.done_control_config).setVisibility(View.VISIBLE); + mInputOverlay.setIsInEditMode(true); + } + + public void stopConfiguringControls() { + getView().findViewById(R.id.done_control_config).setVisibility(View.GONE); + mInputOverlay.setIsInEditMode(false); + } + + public boolean isConfiguringControls() { + return mInputOverlay.isInEditMode(); + } + + private static class EmulationState { + private final String mGamePath; + private State state; + private Surface mSurface; + private boolean mRunWhenSurfaceIsValid; + + EmulationState(String gamePath) { + mGamePath = gamePath; + // Starting state is stopped. + state = State.STOPPED; + } + + public synchronized boolean isStopped() { + return state == State.STOPPED; + } + + // Getters for the current state + + public synchronized boolean isPaused() { + return state == State.PAUSED; + } + + public synchronized boolean isRunning() { + return state == State.RUNNING; + } + + public synchronized void stop() { + if (state != State.STOPPED) { + Log.debug("[EmulationFragment] Stopping emulation."); + state = State.STOPPED; + NativeLibrary.StopEmulation(); + } else { + Log.warning("[EmulationFragment] Stop called while already stopped."); + } + } + + // State changing methods + + public synchronized void pause() { + if (state != State.PAUSED) { + state = State.PAUSED; + Log.debug("[EmulationFragment] Pausing emulation."); + + // Release the surface before pausing, since emulation has to be running for that. + NativeLibrary.SurfaceDestroyed(); + NativeLibrary.PauseEmulation(); + } else { + Log.warning("[EmulationFragment] Pause called while already paused."); + } + } + + public synchronized void run(boolean isActivityRecreated) { + if (isActivityRecreated) { + if (NativeLibrary.IsRunning()) { + state = State.PAUSED; + } + } else { + Log.debug("[EmulationFragment] activity resumed or fresh start"); + } + + // If the surface is set, run now. Otherwise, wait for it to get set. + if (mSurface != null) { + runWithValidSurface(); + } else { + mRunWhenSurfaceIsValid = true; + } + } + + // Surface callbacks + public synchronized void newSurface(Surface surface) { + mSurface = surface; + if (mRunWhenSurfaceIsValid) { + runWithValidSurface(); + } + } + + public synchronized void clearSurface() { + if (mSurface == null) { + Log.warning("[EmulationFragment] clearSurface called, but surface already null."); + } else { + mSurface = null; + Log.debug("[EmulationFragment] Surface destroyed."); + + if (state == State.RUNNING) { + NativeLibrary.SurfaceDestroyed(); + state = State.PAUSED; + } else if (state == State.PAUSED) { + Log.warning("[EmulationFragment] Surface cleared while emulation paused."); + } else { + Log.warning("[EmulationFragment] Surface cleared while emulation stopped."); + } + } + } + + private void runWithValidSurface() { + mRunWhenSurfaceIsValid = false; + if (state == State.STOPPED) { + NativeLibrary.SurfaceChanged(mSurface); + Thread mEmulationThread = new Thread(() -> + { + Log.debug("[EmulationFragment] Starting emulation thread."); + NativeLibrary.Run(mGamePath); + }, "NativeEmulation"); + mEmulationThread.start(); + + } else if (state == State.PAUSED) { + Log.debug("[EmulationFragment] Resuming emulation."); + NativeLibrary.SurfaceChanged(mSurface); + NativeLibrary.UnPauseEmulation(); + } else { + Log.debug("[EmulationFragment] Bug, run called while already running."); + } + state = State.RUNNING; + } + + private enum State { + STOPPED, RUNNING, PAUSED + } + } +} |